热修复实现(一)

现在的热修复方案已经有很多了,例如alibabadexposedAndFix以及jasonrossNuwa等等。原来没有仔细去分析过也没想写这篇文章,但是之前InstantRun详解这篇文章中介绍了Android Studio Instant Run的 实现原理,这不就是活生生的一个热修复吗? 随心情久久不能平复,我们何不用这种方式来实现。

方案有很多种,我就只说明下我想到的方式,也就是Instant Run的方式:
分拆到不同的dex中,然后通过classloader来进行加载。但是在之前InstantRun详解中只说到会通过内部的server去判断该类是否有更新,如果有的话就去从新的dex中加载该类,否则就从旧的dex中加载,但这是如何实现的呢? 怎么去从不同的dex中选择最新的那个来进行加载。

讲到这里需要先介绍一下ClassLoader

< A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.

在一般情况下,应用程序不需要创建ClassLoader对象,而是使用当前环境已经存在的ClassLoader。因为JavaRuntime环境在初始化时,其内部会创建一个ClassLoader对象用于加载Runtime所需的各种Java类。
每个ClassLoader必须有一个父类,在装载Class文件时,子ClassLoader会先请求父ClassLoader加载该Class文件,只有当其父ClassLoader找不到该Class文件时,子ClassLoader才会继续装载该类,这是一种安全机制。

对于Android的应用程序,本质上虽然也是用Java开发,并且使用标准的Java编译器编译出Class文件,但最终的APK文件中包含的却是dex类型的文件。dex文件是将所需的所有Class文件重新打包,打包的规则不是简单的压缩,而是完全对Class文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是dex文件。由于dex文件是一种经过优化的Class文件,因此要加载这样特殊的Class文件就需要特殊的类装载器,这就是DexClassLoaderAndroid SDK中提供的DexClassLoader类就是出于这个目的。

总体来说,Android 默认主要有三个ClassLoader:

  • BootClassLoader: 系统启动时创建
    Provides an explicit representation of the boot class loader. It sits at the head of the class loader chain and delegates requests to the VM's internal class loading mechanism.
  • PathClassLoader: 可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
  • DexClassLoader: 可以加载文件系统上的jardexapk;可以从SD卡中加载未安装的apk

通过上面的分析知道,如果用多个dex的话肯定会用到DexClassLoader类,我们首先来看一下它的源码(这里 插一嘴,源码可以去googlesource中找):

/**
 * A class loader that loads classes from {@code .jar} and {@code .apk} files
 * containing a {@code classes.dex} entry. This can be used to execute code not
 * installed as part of an application.
 *
 * <p>This class loader requires an application-private, writable directory to
 * cache optimized classes. Use {@code Context.getDir(String, int)} to create
 * such a directory: <pre>   {@code
 *   File dexOutputDir = context.getDir("dex", 0);
 * }</pre>
 *
 * <p><strong>Do not cache optimized classes on external storage.</strong>
 * External storage does not provide access controls necessary to protect your
 * application from code injection attacks.
 */
public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     *
     * <p>The path lists are separated using the character specified by the
     * {@code path.separator} system property, which defaults to {@code :}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     *     resources, delimited by {@code File.pathSeparator}, which
     *     defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     *     should be written; must not be {@code null}
     * @param libraryPath the list of directories containing native
     *     libraries, delimited by {@code File.pathSeparator}; may be
     *     {@code null}
     * @param parent the parent class loader
     */
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

注释说的太明白了,这里就不翻译了,但是我们并没有找到加载的代码,去它的父类中查找, 因为家在都是从loadClass()方法中,所以我们去ClassLoader类中看一下loadClass()方法:

/**
     * Loads the class with the specified name. Invoking this method is
     * equivalent to calling {@code loadClass(className, false)}.
     * <p>
     * <strong>Note:</strong> In the Android reference implementation, the
     * second parameter of {@link #loadClass(String, boolean)} is ignored
     * anyway.
     * </p>
     *
     * @return the {@code Class} object.
     * @param className
     *            the name of the class to look for.
     * @throws ClassNotFoundException
     *             if the class can not be found.
     */
    public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }

    /**
     * Loads the class with the specified name, optionally linking it after
     * loading. The following steps are performed:
     * <ol>
     * <li> Call {@link #findLoadedClass(String)} to determine if the requested
     * class has already been loaded.</li>
     * <li>If the class has not yet been loaded: Invoke this method on the
     * parent class loader.</li>
     * <li>If the class has still not been loaded: Call
     * {@link #findClass(String)} to find the class.</li>
     * </ol>
     * <p>
     * <strong>Note:</strong> In the Android reference implementation, the
     * {@code resolve} parameter is ignored; classes are never linked.
     * </p>
     *
     * @return the {@code Class} object.
     * @param className
     *            the name of the class to look for.
     * @param resolve
     *            Indicates if the class should be resolved after loading. This
     *            parameter is ignored on the Android reference implementation;
     *            classes are not resolved.
     * @throws ClassNotFoundException
     *             if the class can not be found.
     */
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                // 先检查父ClassLoader是否已经家在过该类
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    // 调用DexClassLoader.findClass()方法。
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

上面会调用DexClassLoader.findClass()方法,但是DexClassLoader没有实现该方法,所以去它的父类BaseDexClassLoader中看,接着看一下BaseDexClassLoader的源码:

/**
 * Base class for common functionality between various dex-based
 * {@link ClassLoader} implementations.
 */
public class BaseDexClassLoader extends ClassLoader {
    /** originally specified path (just used for {@code toString()}) */
    private final String originalPath;
    /** structured lists of path elements */
    private final DexPathList pathList;
    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList =
            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 从DexPathList中找
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }
    @Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }
    @Override
    protected Enumeration<URL> findResources(String name) {
        return pathList.findResources(name);
    }
    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

findClass()方法中我们看到调用了DexPathList.findClass()方法:

/**
 * A pair of lists of entries, associated with a {@code ClassLoader}.
 * One of the lists is a dex/resource path &mdash; typically referred
 * to as a "class path" &mdash; list, and the other names directories
 * containing native code libraries. Class path entries may be any of:
 * a {@code .jar} or {@code .zip} file containing an optional
 * top-level {@code classes.dex} file as well as arbitrary resources,
 * or a plain {@code .dex} file (with no possibility of associated
 * resources).
 *
 * <p>This class also contains methods to use these lists to look up
 * classes and resources.</p>
 */
/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";
    /** class definition context */
    private final ClassLoader definingContext;
    /** list of dex/resource (class path) elements */ 
    // 把dex封装成一个数组,每个Element代表一个dex
    private final Element[] dexElements;
    /** list of native library directory elements */
    private final File[] nativeLibraryDirectories;

    // .....

    /**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @return the named class or {@code null} if the class is not
     * found in any of the dex files
     */
    public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            // 遍历数组,拿到第一个就返回
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }
}

从上面的源码中分析,我知道系统会把所有相关的dex维护到一个数组中,然后在加载类的时候会从该数组中的第一个元素中取,然后返回。那我们只要保证将我们热修复后的dex对应的Element放到该数组的第一个位置就可以了,这样系统就会加载我们热修复的dex中的类。
所以方案出来了,只要把有问题的类修复后,放到一个单独的dex,然后把该Dex转换成对应的Element后再将该Element插入到dexElements数组的第一个位置就可以了。那该如何去将其插入到dexElements数组的第一个位置呢?-- 暴力反射。

到这里我感觉初步的思路已经有了:

  • 将补丁作为dex发布。
  • 通过反射修改该dex所对应的Element在数组中的位置。

但是我也想到肯定还会有类似下面的问题:

  • 资源文件的处理
  • 四大组件的处理
  • 清单文件的处理

虽然我知道没有这么简单,但是我还是决定抱着不作不死的宗旨继续前行。

好了,demo走起来。

怎么生成dex文件呢? 这要讲过两部分:

  • .class-> .jar : jar -cvf test.jar com/charon/instantfix_sample/MainActivity.class
  • .jar-> .dex: dx --dex --output=target.jar test.jar target.jar就是包含.dexjar

生成好dex后我们为了模拟先将其放到asset目录下(实际开发中肯定要从接口中去下载,当然还会有一些版本号的判断等),然后就是将该dex转换成

方案中采用的是MultiDex,对其进行一部分改造,具体代码:

  • 添加dex文件,并执行install
/**
* 添加apk包外的dex文件
* 自动执行install
* @param dexFile
*/
public static void addDexFileAutoInstall(Context context, List<File> dexFile,File optimizedDirectory) {
    if (dexFile != null && !dexFile.isEmpty() &&!dexFiles.contains(dexFile)) {
         dexFiles.addAll(dexFile);
         LogUtil.d(TAG, "add other dexfile");
         installDexFile(context,optimizedDirectory);
     }
}
  • installDexFile直接调用MultiDexinstallSecondaryDexes方法
/**
 * 添加apk包外的dex文件,
 * @param context
 */
publicstatic void installDexFile(Context context, File optimizedDirectory){
    if (checkValidZipFiles(dexFiles)) {
        try {
            installSecondaryDexes(context.getClassLoader(), optimizedDirectory, dexFiles);
        } catch (IllegalAccessExceptione){
           e.printStackTrace();
        } catch (NoSuchFieldExceptione) {
           e.printStackTrace();
        } catch (InvocationTargetExceptione){
           e.printStackTrace();
        } catch (NoSuchMethodExceptione) {
           e.printStackTrace();
        } catch (IOExceptione) {
           e.printStackTrace();
        }
    }
}
  • patch.dex放在所有dex最前面
private static voidexpandFieldArray(Object instance, String fieldName, Object[]extraElements) throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
    Field jlrField = findField(instance, fieldName);
    Object[]original = (Object[]) jlrField.get(instance);
    Object[]combined = (Object[]) Array.newInstance(original.getClass().getComponentType(),original.length + extraElements.length);
    // 将后来的dex放在前面,主dex放在最后。
    System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
    System.arraycopy(original, 0, combined, extraElements.length,original.length);
    // 原始的dex合并,是将主dex放在前面,其他的dex依次放在后面。
    //System.arraycopy(original, 0, combined, 0, original.length);
    //System.arraycopy(extraElements, 0, combined, original.length,extraElements.length);
    jlrField.set(instance, combined);
}

到此将patch.dex放进了Element,接下来的问题就是加载Class,当加载patch.dex中类的时候,会遇到一个问题,这个问题就是QQ空间团队遇到的ClassCLASS_ISPREVERIFIED。具体原因是dvmResolveClass这个方法对Class进行了校验。判断这个要Resolveclass是否和其引用来自一个dex。如果不是,就会遇到问题。

当引用这和被引用者不在同一个dex中就会抛出异常,导致Resolve失败。QQ空间团队的方案是阻止所有的Class类打上CLASS_ISPREVERIFIED来逃过校验,这种方式其实是影响性能。 我们的方案是和QQ团队的类似,但是和QQ空间不同的是,我们将fromUnverifiedConstant设置为true,来逃过校验,达到补丁的路径。具体怎么实现呢? 要引用Cydia Hook技术来hook Native dalvikdvmResolveClass这个方法。有关Cydia Hook技术请参考:

具体代码如下:

//指明要hook的lib :
MSConfig(MSFilterLibrary,"/system/lib/libdvm.so")

// 在初始化的时候进行hook
MSInitialize {
    LOGD("Cydia Init");
    MSImageRef image;
    //载入lib
    image = MSGetImageByName("/system/lib/libdvm.so");
    if (image != NULL) {
        LOGD("image is not null");
        void *dexload=MSFindSymbol(image,"dvmResolveClass");
        if(dexload != NULL) {
            LOGD("dexloadis not null");
            MSHookFunction(dexload, (void*)proxyDvmResolveClass, (void**)&dvmResolveClass_Proxy);
        } else{
            LOGD("errorfind dvmResolveClass");
        }
    }
}
// 在初始化的时候进行hook//保留原来的地址
ClassObject* (*dvmResolveClass_Proxy)(ClassObject* referrer, u4 classIdx, boolfromUnverifiedConstant);
// 新方法地址
static ClassObject* proxyDvmResolveClass(ClassObject* referrer, u4 classIdx,bool fromUnverifiedConstant) {
        return dvmResolveClass_Proxy(referrer, classIdx,true);
}

说到此处,似乎已经是一个完整的方案了,但在实践中,会发现运行加载类的时候报preverified错误,原来在DexPrepare.cpp,将dex转化成odex的过程中,会在DexVerify.cpp进行校验, 验证如果直接引用到的类和clazz是否在同一个dex,如果是,则会打上CLASS_ISPREVERIFIED标志。通过在所有类(Application除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的dex的类的引用,就可以解决这个问题。空间使用了javaassist进行编译时字节码插入。

所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志。 最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下:

if (ClassVerifier.PREVENT_VERIFY) {
    System.out.println(AntilazyLoad.class);
}

其中AntilazyLoad类会被打包成单独的antilazy.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。 然后在应用启动的时候加载进来AntilazyLoad类所在的dex包必须被先加载进来,不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dex包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad找不到的log。 所以Application作为应用的入口不能插入这段代码。(因为载入hack.dex的代码是在ApplicationonCreate中执行的,如果在Application的构造函数里面插入了这段代码,那么就是在hack.dex加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志)

如何打包补丁包:

1. 空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有class文件的md5,还有一份mapping混淆文件。 2. 在后续的版本中使用-applymapping选项,应用正式版本的mapping文件,然后计算编译完成后的class文件的md5和正式版本进行比较,把不相同的class文件打包成补丁包。 备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包dex,只需要把修改过的类的class文件打包成patch dex,然后放到sdcard下,那么就会让改变的代码生效。

Java中,只有当两个实例的类名、包名以及加载其的ClassLoader都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。 同一个Class=相同的ClassName + PackageName + ClassLoader 以上问题在采用动态加载功能的开发中容易出现,请注意。

通过上面的分析,我们知道使用ClassLoader动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的Java程序,在Android程序中使用动态加载主要有两个麻烦的问题:

  • Android中许多组件类(如Activity、Service等)是需要在Manifest文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作;
  • Res资源是Android开发中经常用到的,而Android是把这些资源用对应的R.id注册好,运行时通过这些IDResource实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到R.id的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的Resource实例中保存的资源ID对不上;

说到底,抛开虚拟机的差别不说,一个Android程序和标准的Java程序最大的区别就在于他们的上下文环境(Context)不同。Android中,这个环境可以给程序提供组件需要用到的功能,也可以提供一些主题、Res等资源,其实上面说到的两个问题都可以统一说是这个环境的问题,而现在的各种Android动态加载框架中,核心要解决的东西也正是“如何给外部的新类提供上下文环境”的问题。

DexClassLoader的使用方法一般有两种:

  • 从已安装的apk中读取dex
  • apk文件中读取dex

假如有两个APK,一个是宿主APK,叫作HOST,一个是插件APK,叫作PluginPlugin中有一个类叫PluginClass,代码如下:

public class PluginClass {
    public PluginClass() {
        Log.d("JG","初始化PluginClass");
    }

    public int function(int a, int b){
        return a+b;  
    }  
}

现在如果想调用插件APKPluginClass内的方法,应该怎么办?

从已安装的apk中读取dex

先来看第一种方法,这种方法必须建一个Activity,在清单文件中配置Action.

<activity android:name=".MainActivity">
         <intent-filter>
             <action android:name="com.maplejaw.plugin"/>
         </intent-filter>
</activity>

然后在宿主APK中如下使用:

/**
   * 这种方式用于从已安装的apk中读取,必须要有一个activity,且需要配置ACTION
   */
private void useDexClassLoader(){
      //创建一个意图,用来找到指定的apk
      Intent intent = new Intent("com.maplejaw.plugin");
      //获得包管理器
      PackageManager pm = getPackageManager();
      List<ResolveInfo> resolveinfoes =  pm.queryIntentActivities(intent, 0);
      if(resolveinfoes.size()==0){
          return;
      }
      //获得指定的activity的信息
      ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;

      //获得包名
      String packageName = actInfo.packageName;
      //获得apk的目录或者jar的目录
      String apkPath = actInfo.applicationInfo.sourceDir;
      //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己
      //目录下的文件
      String dexOutputDir = getApplicationInfo().dataDir;

      //native代码的目录
      String libPath = actInfo.applicationInfo.nativeLibraryDir;

      //创建类加载器,把dex加载到虚拟机中
      DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath,
              this.getClass().getClassLoader());

      //利用反射调用插件包内的类的方法

      try {
          Class<?> clazz = calssLoader.loadClass(packageName+".PluginClass");

          Object obj = clazz.newInstance();
          Class[] param = new Class[2];
          param[0] = Integer.TYPE;
          param[1] = Integer.TYPE;

          Method method = clazz.getMethod("function", param);

          Integer ret = (Integer)method.invoke(obj, 12,34);

          Log.d("JG", "返回的调用结果为:" + ret);

      } catch (Exception e) {
          e.printStackTrace();
      } 
  }

我们安装完两个APK后,在宿主中就可以直接调用,调用示例如下。

public void btnClick(View view){
    useDexClassLoader();
}

从apk文件中读取dex

这种方法由于并不需要安装,所以不需要通过Intentactivity中解析信息。换言之,这种方法不需要创建Activity。无需配置清单文件。我们只需要打包一个apk,然后放到SD卡中即可。 核心代码如下:

//apk路径
String path=Environment.getExternalStorageDirectory().getAbsolutePath()+"/1.apk";

private void useDexClassLoader(String path){

    File codeDir=getDir("dex", Context.MODE_PRIVATE);

    //创建类加载器,把dex加载到虚拟机中
    DexClassLoader calssLoader = new DexClassLoader(path, codeDir.getAbsolutePath(), null,
            this.getClass().getClassLoader());

    //利用反射调用插件包内的类的方法

    try {
        Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass");

        Object obj = clazz.newInstance();
        Class[] param = new Class[2];
        param[0] = Integer.TYPE;
        param[1] = Integer.TYPE;

        Method method = clazz.getMethod("function", param);

        Integer ret = (Integer)method.invoke(obj, 12,21);

        Log.d("JG", "返回的调用结果为: " + ret);

    } catch (Exception e) {
        e.printStackTrace();
    }

动态加载的几个关键问题:

  • 资源访问:无法找到某某id所对应的资源 因为将apk加载到宿主程序中去执行,就无法通过宿主程序的Context去取到apk中的资源,比如图片、文本等,这是很好理解的,因为apk已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文, 用别人的Context是无法得到自己的资源的

    • 解决方案一:插件中的资源在宿主程序中也预置一份;
      缺点:增加了宿主apk的大小;在这种模式下,每次发布一个插件都需要将资源复制到宿主程序中,这意味着每发布一个插件都要更新一下宿主程序;
    • 解决方案二:将插件中的资源解压出来,然后通过文件流去读取资源;
      缺点:实际操作起来还是有很大难度的。首先不同资源有不同的文件流格式,比如图片、XML等,其次针对不同设备加载的资源可能是不一样的,如何选择合适的资源也是一个需要解决的问题; 实际解决方案:
      Activity中有一个叫mBase的成员变量,它的类型就是ContextImpl。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上Context就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl中;
/** Return an AssetManager instance for your application's package. */
    public abstract AssetManager getAssets();

    /** Return a Resources instance for your application's package. */
    public abstract Resources getResources();

具体实现:

protected void loadResources() {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, mDexPath);
        mAssetManager = assetManager;
    } catch (Exception e) {
        e.printStackTrace();
    }
    Resources superRes = super.getResources();
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
            superRes.getConfiguration());
    mTheme = mResources.newTheme();
    mTheme.setTo(super.getTheme());
}

加载资源的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources对象中,由于addAssetPath是隐藏API我们无法直接调用,所以只能通过反射。

@hide
    public final int addAssetPath(String path) {
    synchronized (this) {
        int res = addAssetPathNative(path);
        makeStringBlocks(mStringBlocks);
        return res;
    }
}

Activity生命周期的管理:反射方式和接口方式。
反射的方式很好理解,首先通过Java的反射去获取Activity的各种生命周期方法,比如onCreateonStartonResume等,然后在代理Activity中去调用插件Activity对应的生命周期方法即可:
缺点:一方面是反射代码写起来比较复杂,另一方面是过多使用反射会有一定的性能开销。

反射方式

@Override
protected void onResume() {
    super.onResume();
    Method onResume = mActivityLifecircleMethods.get("onResume");
    if (onResume != null) {
        try {
            onResume.invoke(mRemoteActivity, new Object[] { });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

@Override
protected void onPause() {
    Method onPause = mActivityLifecircleMethods.get("onPause");
    if (onPause != null) {
        try {
            onPause.invoke(mRemoteActivity, new Object[] { });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    super.onPause();
}

接口方式

public interface DLPlugin {

    public void onStart();

    public void onRestart();

    public void onActivityResult(int requestCode, int resultCode, Intent

    data);

    public void onResume();

    public void onPause();

    public void onStop();

    public void onDestroy();

    public void onCreate(Bundle savedInstanceState);

    public void setProxy(Activity proxyActivity, String dexPath);

    public void onSaveInstanceState(Bundle outState);

    public void onNewIntent(Intent intent);

    public void onRestoreInstanceState(Bundle savedInstanceState);

    public boolean onTouchEvent(MotionEvent event);

    public boolean onKeyUp(int keyCode, KeyEvent event);

    public void onWindowAttributesChanged(LayoutParams params);

    public void onWindowFocusChanged(boolean hasFocus);

    public void onBackPressed();

…

}

代理Activity中只需要按如下方式即可调用插件Activity的生命周期方法,这就完成了插件Activity的生命周期的管理;插件Activity需要实现DLPlugin接口:

@Override
protected void onStart() {
    mRemoteActivity.onStart();
    super.onStart();
}

@Override
protected void onRestart() {
    mRemoteActivity.onRestart();
    super.onRestart();
}

@Override
protected void onResume() {
    mRemoteActivity.onResume();
    super.onResume();
}

results matching ""

    No results matching ""